package edu.nps.ai.astar;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * Holds the node information of the Wham Doc
 *
 * @author Timothy Riley
 */
public class Node implements Comparable<Node> {
	/**
	 * Holds the data
	 */
	private Point data;

	/**
	 * Holds the parent node
	 */
	private Node parent;

	/**
	 * Holds the list of children
	 */
	private List<Node> childrenList = new ArrayList<Node>();

	/**
	 * Holds the path cost
	 */
	private Integer pathCost;

	/**
	 * Holds the estimated remaining cost
	 */
	private Integer remainingCost;

	/**
	 * Holds the total Cost
	 */
	private Integer totalCost;

	/**
	 * A node is made up of a data point and it's children and parent node
	 * @param dataPoint The data point
	 */
	public Node(final Point dataPoint) {
		if (dataPoint == null) {
			throw new IllegalArgumentException("The data point and remaining cost are required");
		}
		this.data = dataPoint;
	}


	/**
	 * @return the data
	 */
	public Point getData() {
		return data;
	}


	/**
	 * @param data the data to set
	 */
	public void setData(Point data) {
		this.data = data;
	}


	/**
	 * @return the parent
	 */
	public Node getParent() {
		return parent;
	}


	/**
	 * @param parent the parent to set
	 */
	public void setParent(Node parent) {
		this.parent = parent;
	}


	/**
	 * @return the childrenList
	 */
	public List<Node> getChildrenList() {
		return childrenList;
	}


	/**
	 * @param childrenList the childrenList to set
	 */
	public void setChildrenList(List<Node> childrenList) {
		this.childrenList = childrenList;
	}

	/**
	 * Helper method to add a child to the node
	 * @param child The child node
	 */
	public Boolean addChildToList(Node child) {
		if (child == null) {
			throw new IllegalArgumentException("The child is required");
		}
		//Exclude ancestor's
		Boolean ancestor = ancestorNode(child);
		Boolean added = false;
		if (!ancestor) {
			calculateChildCost(child);
			added = childrenList.add(child);
		}
		return added;

	}

	/**
	 * @return the pathCost
	 */
	public Integer getPathCost() {
		return pathCost;
	}

	/**
	 * @param pathCost
	 *            the pathCost to set
	 */
	public void setPathCost(final Integer pathCost) {
		this.pathCost = pathCost;
	}

	/**
	 * @return the estimated remaining cost
	 */
	public Integer getRemainingCost() {
		return remainingCost;
	}

	/**
	 * @param remainingCost
	 *            the estimated remaining cost to set
	 */
	public void setRemainingCost(final Integer remainingCost) {
		this.remainingCost = remainingCost;
	}

	/**
	 * @return the totalCost
	 */
	public Integer getTotalCost() {
		return totalCost;
	}

	/**
	 * @param totalCost the totalCost to set
	 */
	public void setTotalCost(final Integer totalCost) {
		this.totalCost = totalCost;
	}

	/**
	 * Helper method to calculate the path cost between two nodes
	 * The path cost is defined as the Euclidean distance between
	 * points p and q is the length of the line segment connecting them.
	 * @param successorNode The successor node
	 */
	private void calculateChildPathCost(final Node childNode) {
		if (childNode == null) {
			throw new IllegalArgumentException("The child node is required");
		}
		Point childPoint = childNode.getData();
		if (data == null || childPoint == null) {
			throw new IllegalStateException("The data and successor data are required");
		}
		Integer x = data.getX();
		Integer y = data.getY();
		Integer z = data.getZ();
		Integer childX = childPoint.getX();
		Integer childY = childPoint.getY();
		Integer childZ = childPoint.getZ();
		if (x == null || y == null || z == null || childX == null || childY == null || childZ == null) {
			throw new IllegalStateException("The successor and current  x, y and z coordinates are required");
		}
		int childPathCost = (int) Math.sqrt(Math.pow((x - childX), 2) + Math.pow((y - childY), 2) + Math.pow((z - childZ), 2));
		childNode.setPathCost(pathCost + childPathCost);
	}

	/**
	 * Calculate the total cost by summing the path cost provided with the
	 * previously estimated remaining cost
	 * @param pathCost The path cost
	 */
	private void calculateChildCost(final Node childNode) {
		if (childNode == null) {
			throw new IllegalArgumentException("The child node is required");
		}
		calculateChildPathCost(childNode);
		Integer childPathCost = childNode.getPathCost();
		Integer childRemainingCost = childNode.getRemainingCost();
		if (childPathCost == null || childRemainingCost == null) {
			throw new IllegalStateException("The path and remaining cost are required");
		}
		childNode.setTotalCost(childPathCost + childRemainingCost);
 	}

	/**
	 * Helper method to determine if the node provided is in the ancestor list
	 * @param nodeToCheck The node to check
	 * @return True if the node is in the ancestor list
	 */
	private Boolean ancestorNode(Node child) {
		if (child == null) {
			throw new IllegalArgumentException("The node to check is required");
		}
		Boolean ancestor = false;
		if (parent != null) {
			Point childPoint = child.getData();
			Point parentPoint = parent.getData();
			if (parentPoint.equals(childPoint)) {
				ancestor = true;
			} else {
				ancestor = parent.ancestorNode(child);
			}
		}
		return ancestor;
	}

	/**
	 * Implementation of the toString method
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Processing " + data.toString() + ", g=" + pathCost + ", h=" + remainingCost + ", f=" + totalCost;
	}

	/**
	 * Implementation of the hash code method
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(1, 31);
		hashCodeBuilder.append(this.totalCost);
		return hashCodeBuilder.toHashCode();
	}

	/**
	 * Implementation of the is equals method
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (null == obj) {
			return false;
		}
		if (obj instanceof Node == false) {
			return false;
		}
		if (this == obj) {
			return true;
		}
		Node compareNode = (Node) obj;
		EqualsBuilder equalsBuilder = new EqualsBuilder();
		equalsBuilder.append(this.totalCost, compareNode.getTotalCost());
		return equalsBuilder.isEquals();
	}

	/**
	 * Implementation of compareTo
	 */
	public int compareTo(Node compareNode) {
		CompareToBuilder compareToBuilder = new CompareToBuilder();
		compareToBuilder.append(this.totalCost, compareNode.getTotalCost());
		return compareToBuilder.toComparison();
	}
}
